package projects.ufam.nodes.nodeImplementations;

import java.awt.Color;
import java.awt.Graphics;

import projects.defaultProject.models.connectivityModels.QUDG;
import projects.defaultProject.models.connectivityModels.UDG;
import projects.defaultProject.models.energyConsumptionModels.InfinitePowerSupply;
import projects.defaultProject.models.energyConsumptionModels.RealisticConsumptionModel;
import projects.defaultProject.models.energyConsumptionModels.TxRxConsumptionModel;
import projects.defaultProject.nodes.nodeImplementations.LocalizableNode;
import projects.defaultProject.nodes.timers.WaitTimer;
import projects.defaultProject.nodes.timers.WakeupTimer;
import projects.ufam.CustomGlobal;
import projects.ufam.nodes.messages.GAFMessage;
import sinalgo.configuration.Configuration;
import sinalgo.configuration.WrongConfigurationException;
import sinalgo.gui.transformation.PositionTransformation;
import sinalgo.nodes.Node;
import sinalgo.nodes.NodeState;
import sinalgo.nodes.messages.Message;
import sinalgo.nodes.timers.Timer;
import sinalgo.tools.Tools;

public class GAFNode extends LocalizableNode {
	public static double cellSize = 0.0;
	public static int cols = 0, rows = 0;

	public GAFNode() {
	}

	@Override
	public void onLocalized() {
		super.onLocalized();
	}

	public void startDensityControl() {
		if (this == CustomGlobal.sink)
			return;
		Integer grid = getGridId(false);
		if (grid != null) {
			this.switchState(NodeState.WakingUp);
			Message m = new GAFMessage(this, grid == null ? 0 : grid);
			WaitTimer wt = new WaitTimer(m);
			wt.startRelative(getDiscoveryInterval(), this);
		}
	}

	public int getDiscoveryInterval() {
		if (this.getState() == NodeState.WakingUp) {
			double next = 20;
			if (CustomGlobal.useResidualOnDC) {
				if (this.getResidual() > 0) {
					next = 20.0 * this.getResidual();
					next = next < 1 ? 1 : next;
					next = next > 40 ? 40 : next;
				}
			}
			return (int) 1
					+ Tools.getRandomNumberGenerator().nextInt((int) next);
		} else {
			int interval = getTimeActive(this.getEnergyLevel()) / 2;
			if (interval <= 1)
				interval = 2;
			interval += Tools.getRandomNumberGenerator().nextInt(interval / 2);
			return interval;
		}
	}

	public Integer getGridId(boolean real) {
		if (!real) {
			if (isFreeNode())
				return null;
		}
		// Nó não foi localizado
		if (getEstimatedPosition() == null)
			return null;
		if (cellSize == 0) {
			if (CustomGlobal.targetEnabled && CustomGlobal.kCoverage >= 3) {
				// Aproximei o observado no sinalgo à uma exponencial inversa:
				// A k-cobertura pode ser escrita em função da razão entre o
				// tamanho da célula e o raio de comunicação (x):
				// k = f(x) = 41.486 + exp (-4.4713 * x) + 1.3738
				double a = 41.486;
				double b = -4.4713;
				double c = 1.3738;
				double d = (((double) Tools.getNodeList().size()) / (double) (Configuration.dimX * Configuration.dimY)); // node
				// density
				cellSize = Math.log(((double) CustomGlobal.kCoverage - c) / a)
						/ b;
				cellSize *= CustomGlobal.sensorRange;
			} else {
				if (this.getConnectivityModel() instanceof UDG) {
					double h = ((UDG) this.getConnectivityModel())
							.getMaxTransmissionRange();
					cellSize = h / Math.sqrt(5.0);
				} else if (this.getConnectivityModel() instanceof QUDG) {
					double h = QUDG.getR_min();
					cellSize = h / Math.sqrt(5.0);
				}
			}
			cols = (int) (((double) Configuration.dimX) / cellSize);
		}
		int result = 0;

		int col = (int) (((double) getEstimatedPosition().xCoord) / cellSize);
		int row = (int) (((double) getEstimatedPosition().yCoord) / cellSize);
		if (real) {
			col = (int) (((double) getPosition().xCoord) / cellSize);
			row = (int) (((double) getPosition().yCoord) / cellSize);
		}
		result = col + cols * row;
		return result;
	}

	@Override
	public void checkRequirements() throws WrongConfigurationException {
		// TODO Auto-generated method stub

	}

	public void handleMessage(Message msg, Node sender) {
		if (msg instanceof GAFMessage) {
			handleGAFMessage((GAFMessage) msg, (LocalizableNode) sender);
		} else
			super.handleMessage(msg, sender);
	}

	private void handleGAFMessage(GAFMessage gm, LocalizableNode sender) {
		if (isFreeNode())
			return;
		if (gm.getGridId() != this.getGridId(false))
			return;
		if (this == CustomGlobal.sink)
			return;
		int sleepTime = getTimeActive(gm.getEnat()) / 2;
		if (sleepTime <= 0)
			return;
		sleepTime += Tools.getRandomNumberGenerator().nextInt(sleepTime);

		if (getState() == NodeState.WakingUp) {
			if (!CustomGlobal.useResidualOnDC) {
				Timer timer = null;
				this.switchState(NodeState.StandBy);
				timer = new WakeupTimer();
				timer.startRelative(sleepTime, this);
			} else {
				if (sender.getResidual() < this.getResidual()) {
					Timer timer = null;
					this.switchState(NodeState.StandBy);
					timer = new WakeupTimer();
					timer.startRelative(sleepTime, this);
				}
			}
		} else if (getState() == NodeState.Enabled) {
			if (!CustomGlobal.useResidualOnDC) {
				if (gm.getId() > this.ID) {
					Timer timer = null;
					this.switchState(NodeState.StandBy);
					timer = new WakeupTimer();
					timer.startRelative(sleepTime, this);
				} else {
					GAFMessage m = new GAFMessage(this, getGridId(false));
					this.broadcast(m);
				}
			} else {
				if (sender.getResidual() <= this.getResidual()) {
					Timer timer = null;
					this.switchState(NodeState.StandBy);
					timer = new WakeupTimer();
					timer.startRelative(sleepTime, this);
				}
			}
		}
	}

	public int getTimeActive(Double enat) {
		if (enat <= 0)
			return 0;

		// Adiciona residual, conforme o nakamura pediu
		if (CustomGlobal.useResidualOnDC)
			enat /= getResidual() < 0.01 ? 0.1 : (getResidual() > 500 ? 10
					: getResidual());

		if (this.getEnergyConsumptionModel() instanceof InfinitePowerSupply)
			return Integer.MAX_VALUE;
		else if (this.getEnergyConsumptionModel() instanceof TxRxConsumptionModel) {
			double d = enat * 1.0
					/ this.getEnergyConsumptionModel().getJoulesPerByteTx();
			return (int) d;
		} else if (this.getEnergyConsumptionModel() instanceof RealisticConsumptionModel) {
			double d = enat * 1.0
					/ this.getEnergyConsumptionModel().getJoulesEnabled();
			return (int) d;
		} else
			return Integer.MAX_VALUE;
	}

	public void postStep() {
		if (this.getState() == NodeState.StandBy && !this.isFreeNode()) {
			if (this.getTimers().size() == 0) {
				WakeupTimer timer = new WakeupTimer(); // According to the specs
				// on the GAF paper
				int sleepTime = Tools.getRandomNumberGenerator().nextInt(30);
				sleepTime += 10;
				timer.startRelative(sleepTime, this);
			}
		}
	}

	@Override
	public void wakeUp() {
		Message m = new GAFMessage(this, getGridId(false));
		this.switchState(NodeState.WakingUp);
		WaitTimer wt = new WaitTimer(m);
		wt.startRelative(getDiscoveryInterval(), this);
	}

	public String toString() {
		return "Node(ID=" + this.ID + ", Real Cell=" + getGridId(true)
				+ ", Estimated Cell=" + getGridId(false) + ", Energy Level="
				+ getEnergyLevel() + ")";
	}

	public void afterWait(Message m) {
		if (getState().isEnabled()) {
			switchState(NodeState.Enabled);
			broadcast(m);
		}
		Integer grid = getGridId(false);
		Message m2 = new GAFMessage(this, grid == null ? 0 : grid);
		WaitTimer wt = new WaitTimer(m2);
		wt.startRelative(getDiscoveryInterval(), this);
	}

	public void draw(Graphics g, PositionTransformation pt, boolean highlight) {
		if (this.ID == Tools.getNodeList().size() && cellSize != 0) {
			int x1, x2, y1, y2;
			for (double x = 0; x < Configuration.dimX; x += cellSize) {
				pt.translateToGUIPosition(x, 0, 0);
				y1 = pt.guiY;
				x1 = pt.guiX;
				pt.translateToGUIPosition(x, Configuration.dimY, 0);
				y2 = pt.guiY;
				x2 = x1;
				g.setColor(Color.BLACK);
				g.drawLine(x1, y1, x2, y2);
			}
			for (double y = 0; y < Configuration.dimY; y += cellSize) {
				pt.translateToGUIPosition(0, y, 0);
				y1 = pt.guiY;
				x1 = pt.guiX;
				pt.translateToGUIPosition(Configuration.dimY, y, 0);
				x2 = pt.guiX;
				y2 = y1;
				g.setColor(Color.BLACK);
				g.drawLine(x1, y1, x2, y2);
			}
		}
		super.draw(g, pt, highlight);

	}
}
